// Copyright (C) 2009 Jesse Jones
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;

namespace MmmDoc
{
	/// <summary>Encapsulates an hunspell Hunhandle.</summary>
	internal sealed class HunspellHandle : SafeHandle
	{
		public HunspellHandle() : base(IntPtr.Zero, false)
		{
		}
		
		public HunspellHandle(string affpath, string dicpath) : base(IntPtr.Zero, true)
		{
			handle = Hunspell_create(affpath, dicpath);
			if (IsInvalid)
				throw new InvalidOperationException("Couldn't load hunspell.");
		}
		
		public override bool IsInvalid
		{
			get {return handle == IntPtr.Zero;}
		}
		
		protected override bool ReleaseHandle()
		{
			Hunspell_destroy(this);
			
			return true;
		}
		
		#region P/Invokes
		[DllImport("hunspell-1.2", CharSet = CharSet.Auto)]
		private static extern IntPtr Hunspell_create(string affpath, string dpath);
		
		[DllImport("hunspell-1.2")]
		private static extern void Hunspell_destroy(HunspellHandle handle);
		#endregion
	}
	
	/// <summary>Spell checks words using hunspell.</summary>
	internal sealed class Hunspell
	{
		/// <remarks>Language should be something like 'en_US'.</remarks>
		public static void Init(string language)
		{
			Contract.Requires(ms_instance == null, "don't call Init twice");
			
			try
			{
				ms_instance = new Hunspell(language);
				
				if (Settings.ExtraDictFile != null)
					DoAddExtraWords(Settings.ExtraDictFile);
			}
			catch (DllNotFoundException de)
			{
				if (de.Message.IndexOf(' ') < 0)	// currently mono just has the dll name in DllNotFoundException.Message but this may change in the future
				{
					Console.Error.WriteLine("Couldn't load the {0} library.", de.Message);
				}
				else
				{
					Console.Error.WriteLine("Couldn't load the hunspell library.");
					Console.Error.WriteLine(de.Message);
				}
			}
			catch (FileNotFoundException fe)
			{
				Console.Error.WriteLine("Couldn't initialize the hunspell library.");
				Console.Error.WriteLine(fe.Message);
			}
			catch (InvalidOperationException ie)
			{
				Console.Error.WriteLine(ie.Message);
			}
		}
		
		/// <remarks>Returns null if hunspell could not be loaded.</remarks>
		public static Hunspell Instance
		{
			get {return ms_instance;}
		}
		
		/// <summary>Returns a list of misspelled words in the text.</summary>
		public string[] Check(string text)
		{
			var words = new List<string>();
			
			int index = 0;
			while (index < text.Length)
			{
				if (DoIsWordStart(text, index))
				{
					int count = DoGetWordLength(text, index);
					string word = text.Substring(index, count);
					index += count;
					
					count = DoSkipMarkup(text, index);
					if (count == 0)
					{
						word = word.Trim('\'', '-');
						if (words.IndexOf(word) < 0 && !DoCheckWord(word))
							words.Add(word);
					}
					else
					{
						index += count;
					}
				}
				else
				{
					int count = Math.Max(1, DoSkipMarkup(text, index));
					index += count;
				}
			}
			
			return words.ToArray();
		}
		
		/// <summary>Returns suggested spellings for a word.</summary>
		public string[] Suggest(string word)
		{
			var suggestions = new List<string>();
			
			int ptrSize = Marshal.SizeOf(typeof(IntPtr));
			IntPtr slst = Marshal.AllocHGlobal(ptrSize);
			int count = Hunspell_suggest(m_speller, slst, word);
			
			if (count > 0)
			{
				IntPtr sa = Marshal.ReadIntPtr(slst);
				for (int i = 0; i < count; ++i)
				{
					IntPtr sp = Marshal.ReadIntPtr(sa, i*ptrSize);
					string suggestion = Marshal.PtrToStringAuto(sp);
					suggestions.Add(suggestion);
				}
				Hunspell_free_list(m_speller, slst, count);
			}
			
			Marshal.FreeHGlobal(slst);
			
			return suggestions.ToArray();
		}
		
		#region Private Methods
		private Hunspell(string language)
		{
			Contract.Requires(!string.IsNullOrEmpty(language), "language is null or empty");
			
			string affpath = DoBuildPath(language, "aff");
			string dicpath = DoBuildPath(language, "dic");
			
			m_speller = new HunspellHandle(affpath, dicpath);
		}
		
		private static string DoBuildPath(string language, string ext)
		{
			var paths = new List<string>();
			
			// --dicts
			if (Settings.DictsDir != null)
				paths.Add(Settings.DictsDir);
			
			// assembly's directory
			string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
			path = Path.GetDirectoryName(path);
			paths.Add(path);
			
			// DICPATH environment variable
			path = System.Environment.GetEnvironmentVariable("DICPATH");
			if (!string.IsNullOrEmpty(path))
				paths.Add(path);
			
			// usr paths the hunspell tool checks
			paths.Add("/usr/share/hunspell");			// TODO: where are these installed on windows?
			paths.Add("/usr/share/myspell");
			paths.Add("/usr/share/myspell/dicts");
			
			// Find the file in the first directory listed above.
			string file = language + "." + ext;
			foreach (string candidate in paths)
			{
				path = Path.Combine(candidate, file);
				if (File.Exists(path))
					return path;
			}
			
			string mesg = string.Format("Couldn't find '{0}' in {1}.", file, string.Join(":", paths.ToArray()));
			throw new FileNotFoundException(mesg);
		}
		
		private static void DoAddExtraWords(string extraDict)
		{
			string[] lines = File.ReadAllLines(extraDict);
			for (int i = 0; i < lines.Length; ++i)
			{
				string line = lines[i];
				
				if (line.Length > 0 && line[0] != '#')
				{
					string[] words = line.Split(new char[]{' ', '\t'}, StringSplitOptions.RemoveEmptyEntries);
					if (words.Length == 1)
						Hunspell_add(ms_instance.m_speller, words[0]);	// the c++ API has add_dic, but the C interface does not...
					else if (words.Length == 2)
						Hunspell_add_with_affix(ms_instance.m_speller, words[0], words[1]);
					else
						Console.WriteLine("Expected one or two words on line {0}, not {1} words.", i + 1, words.Length);
				}
			}
		}
		
		private bool DoCheckWord(string word)
		{
			// If the word is mixed case or has numbers call it good.
			for (int i = 1; i < word.Length; ++i)
			{
				if (char.IsUpper(word[i]) || char.IsNumber(word[i]))
					return true;
			}
			
			int result = Hunspell_spell(m_speller, word);
			GC.KeepAlive(this);
			return result != 0;
		}
		
		private static int DoSkipMarkup(string text, int index)
		{
			int count = 0;
			
			if (index < text.Length && text[index] == '<')
			{
				while (index + count < text.Length && text[index + count] != '>')
				{
					++count;
				}
				
				if (index + count < text.Length && text[index + count] == '>')
					++count;
				else
					count = 0;
			}
			
			return count;
		}
		
		private static bool DoIsWordStart(string text, int index)
		{
			char ch = text[index];
			switch (char.GetUnicodeCategory(ch))
			{
				case UnicodeCategory.LowercaseLetter:
				case UnicodeCategory.TitlecaseLetter:
				case UnicodeCategory.UppercaseLetter:
					return true;
			}
			
			return false;
		}
		
		private static int DoGetWordLength(string text, int index)
		{
			int count = 0;
			
			while (index + count < text.Length)
			{
				char ch = text[index + count];
				switch (char.GetUnicodeCategory(ch))
				{
//					case UnicodeCategory.DashPunctuation:
					case UnicodeCategory.DecimalDigitNumber:
					case UnicodeCategory.LowercaseLetter:
					case UnicodeCategory.NonSpacingMark:
					case UnicodeCategory.TitlecaseLetter:
					case UnicodeCategory.UppercaseLetter:
						++count;
						break;
						
					case UnicodeCategory.OtherPunctuation:
						if (ch == '\'')
							++count;
						else
							return count;
						break;
						
					default:
						return count;
				}
			}
			
			return count;
		}
		#endregion
		
		#region P/Invokes
		[DllImport("hunspell-1.2", CharSet = CharSet.Auto)]
		private static extern int Hunspell_spell(HunspellHandle handle, string word);
		
		[DllImport("hunspell-1.2", CharSet = CharSet.Auto)]
		private static extern int Hunspell_suggest(HunspellHandle handle, IntPtr slst, string word);
		
		[DllImport("hunspell-1.2")]
		private static extern void Hunspell_free_list(HunspellHandle handle, IntPtr slst, int n);
		
		[DllImport("hunspell-1.2", CharSet = CharSet.Auto)]
		private static extern int Hunspell_add(HunspellHandle handle, string word);
		
		[DllImport("hunspell-1.2", CharSet = CharSet.Auto)]
		private static extern int Hunspell_add_with_affix(HunspellHandle handle, string word, string example);
		#endregion
		
		#region Fields
		private HunspellHandle m_speller;
		private static Hunspell ms_instance;
		#endregion
	}
}
